/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.validator;

import java.lang.annotation.*;
import java.util.*;
import java.util.concurrent.*;
import ch.msoftch.internal.*;
import ch.trackedbean.internal.*;
import ch.trackedbean.validator.defaultValidators.*;
import ch.trackedbean.validator.internal.*;

/**
 * Manager holding {@link IValidator}s.<br>
 * 
 * To use a own {@link IValidatorFactory} simply create a service provider file for it:<br>
 * Thus is a <code>META-INF/services/ch.trackedbean.validator.IValidatorFactory</code> file containing the fully qualified name of your implementation.<br>
 * 
 * To use a own {@link IMessageInterpolator} simply create a service provider file for it:<br>
 * Thus is a <code>META-INF/services/ch.trackedbean.validator.IMessageInterpolator</code> file containing the fully qualified name of your implementation.<br>
 * 
 * There is also the possibility to map existing annotations to validators:<br>
 * Simply create <code>META-INF/ch.trackedbean.Validator.properties</code> to define them.<br>
 * Here an example of such a file:<br>
 * 
 * <pre>
 * ###Mapping from 'external' annotations to validators###
 * #List defining the following properties
 * annotationMappings=an1,an2
 * #Entries to map the annotation EMail to the MailValidator
 * an1.annotation=ch.myAnnotations.EMail
 * an1.validator=ch.trackedbean.validator.defaultValidators.MailValidator
 * #Entries to map the annotation Regex to the RegexValidator
 * an2.annotation=ch.myAnnotations.Regex
 * an2.validator=ch.trackedbean.validator.defaultValidators.RegexValidator
 * </pre>
 * 
 * @author M. Hautle
 */
// CONTINUE context dependent validation
@SuppressWarnings("unchecked")
public class ValidatorManager {
    /** Empty validator. */
    private static final IValidator NOP_VALIDATOR = new Validator(Object.class);

    /** Mapping for "external" annotations on defined validators. */
    private static final Map<Class<? extends Annotation>, ExternalAnnotationMapping> VALIDATOR_MAPPING = new ConcurrentHashMap<Class<? extends Annotation>, ExternalAnnotationMapping>();

    /** The factory to use. */
    private static IValidatorFactory factory;

    /** The message interpolator used by the {@link AbstractValidator}s. */
    private static IMessageInterpolator messageInterpolator;

    /** Validator cache. */
    private static final ICache<Class, IValidator> VALIDATOR_CACHE = CacheFactory.createCache(Class.class, IValidator.class);

    static {
        factory = ServiceHelper.getService(IValidatorFactory.class, DefaultFactory.class);
        setMessageInterpolator(ServiceHelper.getService(IMessageInterpolator.class, DefaultInterpolator.class));

        final Properties p = ConfigurationHelper.loadSettings("ch.trackedbean.Validator");
        if (p != null) {
            // external annotations
            for (String m : ConfigurationHelper.getListEntries(p, "annotationMappings")) {
                // TODO parameter mapping
                final Class<Object> an = ConfigurationHelper.getClass(p, m + ".annotation");
                final Class<Object> val = ConfigurationHelper.getClass(p, m + ".validator");
                if (an != null && val != null)
                    addAnnotationMapping(new ExternalAnnotationMapping(an, val));
            }
        }
        VALIDATOR_CACHE.put(Object.class, NOP_VALIDATOR);
    }

    /**
     * Returns the validator for the specified class
     * 
     * @param c The validator target class
     * @return The corresponding validator
     * @throws ValidatorInitialisationException If something went wrong
     */
    public static IValidator getValidator(Class<?> c) throws ValidatorInitialisationException {
        IValidator val = VALIDATOR_CACHE.get(c);
        if (val == null) {
            val = factory.createValidator(c);
            if (!val.doesSomething())
                val = NOP_VALIDATOR;
            VALIDATOR_CACHE.put(c, val);
        }
        return val;
    }

    /**
     * Validates the given bean.<br>
     * Consider using {@link #ensureCorrectness(Object)} for serverside validations.
     * 
     * @param bean The bean to validate
     * @return The validation result or null if null was passed
     * @throws TechnicalException If something went wrong
     */
    public static ValidationResult validate(Object bean) throws TechnicalException {
        if (bean == null)
            return null;
        return getValidator(bean.getClass()).validate(bean);
    }

    /**
     * Ensures that the given bean is valid.<br>
     * If the passed bean contains errors a {@link ValidationException} wrapping the {@link ValidationResult} is thrown.<br>
     * Consider using {@link #validate(Object)} for client side validations.
     * 
     * @param bean The bean
     * @throws ValidationException If the bean is not correct
     * @throws TechnicalException If the bean is null or something went wrong
     */
    public static void ensureCorrectness(Object bean) throws ValidationException {
        if (bean == null)
            throw new IllegalArgumentException("The passed bean was null!");
        final ValidationResult res = getValidator(bean.getClass()).validate(bean);
        if (!res.isBeanValid())
            throw new ValidationException(res);
    }

    /**
     * Validates the business part of the given bean.
     * 
     * @param bean The bean to validate
     * @return The validation result or null if null was passed
     * @throws TechnicalException If something went wrong
     */
    public static ValidationResult validateBusinessPart(Object bean) throws TechnicalException {
        if (bean == null)
            return null;
        return getValidator(bean.getClass()).validateBusinessPart(bean);
    }

    /**
     * Validates a property of the given bean.<br>
     * If one of the passed parameters is null the method will also return null.
     * 
     * @param property The property name
     * @param bean The bean
     * @return null if the property was valid, otherwise the erros descriptions
     * @throws TechnicalException If the property doesen't exists or something went wrong
     */
    public static IErrorDescription[] validate(String property, Object bean) throws TechnicalException {
        if (bean == null || property == null || property.length() == 0)
            return null;
        return getValidator(bean.getClass()).validate(property, bean);
    }

    /**
     * Registers the given external mapping.<br>
     * Use this method for "external" annotations (like HibernateValidator- or JPA-Annotations).
     * 
     * @param mapping The mapping
     */
    public static void addAnnotationMapping(ExternalAnnotationMapping mapping) {
        VALIDATOR_MAPPING.put(mapping.getExternalAnnotationType(), mapping);
    }

    /**
     * Returns the registered validator mapping for the given annotation.
     * 
     * @param <T> The annotation type
     * @param <V> The validator type
     * 
     * @param annotation The annotation to lookup
     * @return The mapped validator mapping or null if no mapping exists
     */
    public static <T extends Annotation, V extends IPropertyValidator> ExternalAnnotationMapping<T, V> getMappedValidator(Class<T> annotation) {
        return VALIDATOR_MAPPING.get(annotation);
    }

    /**
     * Returns the default message interpolator.<br>
     * You may use this interpolator for the implementation of {@link IErrorDescription#getStringRepresentation()}.
     * 
     * @return The message interpolator
     */
    public static IMessageInterpolator getMessageInterpolator() {
        return messageInterpolator;
    }

    /**
     * Sets the default message interpolator.
     * 
     * @param interpolator The interpolator to use
     */
    public static void setMessageInterpolator(IMessageInterpolator interpolator) {
        if (interpolator == null)
            throw new IllegalArgumentException("You must set a non null value as interpolator!");
        messageInterpolator = interpolator;
    }

    /**
     * Returns a string representation of the passed errors.<br>
     * This method simply concatenates the results of {@link IErrorDescription#getStringRepresentation()}.
     * 
     * @param errors The errors or null
     * @return The error string or null if null was passed
     */
    public static String buildErrorString(IErrorDescription[] errors) {
        if (errors == null || errors.length == 0)
            return null;
        final StringBuilder str = new StringBuilder();
        for (IErrorDescription e : errors)
            str.append(e.getStringRepresentation()).append('\n');
        str.setLength(str.length() - 1);
        return str.toString();
    }

    /**
     * Default implementation of {@link IValidatorFactory}.
     * 
     * @author M. Hautle
     */
    public static class DefaultFactory implements IValidatorFactory {
        /**
         * {@inheritDoc}
         */
        @Override
        public IValidator createValidator(Class<?> type) throws ValidatorInitialisationException {
            return new DefaultValidatorConfigurator(type).create();
        }
    }
}
